<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Loading Progress</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
    <style>
        * {
            border: 0;
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        :root {
            --hue: 223;
            --bg: hsl(var(--hue), 10%, 90%);
            --fg: hsl(var(--hue), 10%, 10%);
            --gray1: hsl(var(--hue), 10%, 60%);
            --gray2: hsl(var(--hue), 10%, 40%);
            --neutral: hsl(var(--hue), 10%, 50%);
            --success: hsl(153, 90%, 35%);
            --warning: hsl(33, 90%, 50%);
            --trans-dur: 0.3s;
            --trans-timing: cubic-bezier(0.65, 0, 0.35, 1);
            font-size: clamp(1rem, 0.95rem + 0.25vw, 1.25rem);
        }

        body {
            background-color: var(--bg);
            color: var(--fg);
            display: flex;
            font: 1em/1.5 "DM Sans", sans-serif;
            height: 100vh;
            transition: background-color var(--trans-dur), color var(--trans-dur);
        }

        main {
            display: flex;
            overflow-x: hidden;
            width: 100vw;
            height: 100vh;
        }

        svg polyline {
            transition: stroke var(--trans-dur);
        }

        .icon {
            display: block;
            overflow: visible;
            width: 1.5em;
            height: 1.5em;
            transition: color var(--trans-dur);
        }

        .icon--neutral {
            color: var(--neutral);
        }

        .icon--success {
            color: var(--success);
        }

        .icon--warning {
            color: var(--warning);
        }

        .loading {
            display: flex;
            overflow: hidden;
            height: 100%;
        }

        .loading--done {
            overflow: visible;
            height: 26.25em;
        }

        .loading, .loading__step, .loading__steps {
            width: 100%;
        }

        .loading, .loading__steps {
            margin: auto;
        }

        .loading, .loading__step {
            display: flex;
        }

        .loading__ellipsis {
            display: inline-flex;
        }

        .loading__ellipsis-dot {
            --dot-dur: 2s;
            animation: ellipsis-dot-1 var(--dot-dur) steps(1, end) infinite;
            visibility: hidden;
        }

        .loading__ellipsis-dot:nth-child(2) {
            animation-name: ellipsis-dot-2;
        }

        .loading__ellipsis-dot:nth-child(3) {
            animation-name: ellipsis-dot-3;
        }

        .loading__step {
            gap: 1em;
            padding: 0 1.5em;
            position: absolute;
            top: 0;
            left: 0;
            height: 5.25em;
            transition: opacity var(--trans-dur), transform var(--trans-dur) var(--trans-timing);
        }

        .loading__step-info {
            color: var(--gray2);
            font-size: 0.75em;
            line-height: 1.333;
            opacity: 0;
            transition: color var(--trans-dur), opacity var(--trans-dur);
        }

        .loading__step--in .loading__step-info {
            opacity: 1;
        }

        .loading__step-title {
            font-size: 1.25em;
            font-weight: 500;
            line-height: 1.2;
            margin-bottom: 0.25rem;
        }

        .loading__steps {
            position: relative;
            height: 2.75em;
            max-width: 27em;
        }

        /* Dark theme */
        @media (prefers-color-scheme: dark) {
            :root {
                --bg: hsl(var(--hue), 10%, 10%);
                --fg: hsl(var(--hue), 10%, 90%);
            }

            .loading__step-info {
                color: var(--gray1);
            }
        }

        /* Animations */
        @keyframes ellipsis-dot-1 {
            from {
                visibility: hidden;
            }
            25%, to {
                visibility: visible;
            }
        }

        @keyframes ellipsis-dot-2 {
            from, 25% {
                visibility: hidden;
            }
            50%, to {
                visibility: visible;
            }
        }

        @keyframes ellipsis-dot-3 {
            from, 50% {
                visibility: hidden;
            }
            75%, to {
                visibility: visible;
            }
        }
    </style>
</head>

<body>
<div id="root"></div>

<script type="module">
    import React, {StrictMode, useEffect, useRef, useState} from "https://esm.sh/react";
    import {createRoot} from "https://esm.sh/react-dom/client";

    createRoot(document.getElementById("root")).render(React.createElement(StrictMode, null,
        React.createElement("main", null,
            React.createElement(IconSprites, null),
            React.createElement(Loading, null))));

    function Icon({icon, color}) {
        const colorClass = color ? ` icon--${color}` : "";
        return (React.createElement("svg", {
                className: `icon${colorClass}`,
                width: "16px",
                height: "16px",
                "aria-hidden": "true"
            },
            React.createElement("use", {href: `#${icon}`})));
    }

    function IconSprites() {
        const viewBox = "0 0 16 16";
        const emptyCircleRectAngles = [];
        const emptyCircleRectAngleCount = 16;
        for (let r = 0; r < emptyCircleRectAngleCount; ++r) {
            emptyCircleRectAngles.push(360 / emptyCircleRectAngleCount * r);
        }
        return (React.createElement("svg", {width: "0", height: "0", "aria-hidden": "true"},
            React.createElement("symbol", {id: "check-circle", viewBox: viewBox},
                React.createElement("circle", {fill: "currentcolor", cx: "8", cy: "8", r: "8"}),
                React.createElement("polyline", {
                    fill: "none",
                    stroke: "var(--bg)",
                    strokeLinecap: "round",
                    strokeLinejoin: "round",
                    strokeWidth: "1.5",
                    points: "4 8,7 11,12 5"
                })),
            React.createElement("symbol", {id: "empty-circle", viewBox: viewBox},
                React.createElement("defs", null,
                    React.createElement("rect", {id: "empty-circle-rect", x: "-1", width: "2", height: "2"})),
                React.createElement("g", {
                    fill: "currentcolor",
                    transform: "translate(8,8)"
                }, emptyCircleRectAngles.map((rotation, i) => React.createElement("use", {
                    key: i,
                    href: "#empty-circle-rect",
                    transform: `rotate(${rotation}) translate(0,6)`
                })))),
            React.createElement("symbol", {id: "half-circle", viewBox: viewBox},
                React.createElement("clipPath", {id: "half-circle-clip"},
                    React.createElement("rect", {x: "8", y: "0", width: "8", height: "16"})),
                React.createElement("circle", {
                    fill: "none",
                    stroke: "currentcolor",
                    strokeWidth: "2",
                    cx: "8",
                    cy: "8",
                    r: "7"
                }),
                React.createElement("circle", {
                    fill: "currentcolor",
                    cx: "8",
                    cy: "8",
                    r: "5",
                    clipPath: "url(#half-circle-clip)"
                }))));
    }

    function Loading() {
        const [step, setStep] = useState(0);
        const progressFrame = useRef(0);
        const steps = [
            {
                id: 0,
                state: "waiting",
                title: "Preparing"
            },
            {
                id: 1,
                state: "waiting",
                title: "Downloading",
                filesPreparedMax: 50
            },
            {
                id: 2,
                state: "waiting",
                title: "Analyzing",
                filesPreparedMax: 50
            },
            {
                id: 3,
                state: "waiting",
                title: "Creating"
            },
            {
                id: 4,
                state: "waiting",
                title: "Finalizing"
            },
        ];
        const stepCount = useRef(steps.length);
        const [stepObjects, setStepObjects] = useState(steps);
        const allStepsDone = step === stepCount.current;

        /**
         * Increment file preparation or set the state of a loading step.
         * @param item Loading step
         */
        function updatedItem(item) {
            const {id, state, start, filesPrepared, filesPreparedMax} = item;
            const updated = {id, state};
            if (!start) {
                updated.start = new Date();
                updated.state = "progress";
                // don’t proceed to file preparation if not applicable
                if (!filesPreparedMax)
                    return updated;
            }
            if (filesPreparedMax) {
                // increment prepared files
                const prepared = filesPrepared === undefined ? -1 : filesPrepared;
                const preparedInc = 1;
                updated.filesPrepared = Math.min(prepared + preparedInc, filesPreparedMax);
            }
            if (!filesPreparedMax || updated.filesPrepared === filesPreparedMax) {
                // mark it done if no files to prepare or all files are prepared
                updated.finish = new Date();
                updated.state = "done";
            }
            return updated;
        }
        ;
        useEffect(() => {
            const updatePromise = async (delay = 0) => await new Promise((resolve) => {
                progressFrame.current = setTimeout(resolve, delay);
            }).then(() => {
                setStepObjects((prev) => prev.map((item) => {
                    if (item.id !== step)
                        return item;
                    const updated = updatedItem(item);
                    if (updated.state === "done") {
                        clearTimeout(progressFrame.current);
                        setStep((step) => step + 1);
                    }
                    return Object.assign(Object.assign({}, item), updated);
                }));
                // loop
                if (step === 1 || step === 2)
                    updatePromise(50);
                else if (step < stepCount.current)
                    updatePromise(1500);
            });
            updatePromise();
            return () => clearTimeout(progressFrame.current);
        }, [step]);
        return (React.createElement("div", {className: `loading${allStepsDone ? " loading--done" : ""}`},
            React.createElement("div", {className: "loading__steps"}, stepObjects.map((s, i) => {
                const {state, title, start, finish, filesPrepared, filesPreparedMax} = s;
                let distance = i;
                if (allStepsDone) {
                    // keep the middle step in the center
                    distance -= Math.floor(stepCount.current / 2);
                } else {
                    // allow waiting items to be slightly closer to each other
                    let moveBy = step;
                    if (i > step + 1) {
                        const stepHeight = 5.25;
                        moveBy += (i - (step + 1)) * (1.5 / stepHeight);
                    }
                    distance -= moveBy;
                }
                const fade = allStepsDone ? 0 : Math.abs(i - step);
                return React.createElement(LoadingStepBlock, {
                    key: i,
                    title: title,
                    state: state,
                    distance: distance,
                    fade: fade,
                    start: start,
                    finish: finish,
                    filesPrepared: filesPrepared,
                    filesPreparedMax: filesPreparedMax
                });
            }))));
    }

    function LoadingEllipsis() {
        return (React.createElement("div", {className: "loading__ellipsis"},
            React.createElement("div", {className: "loading__ellipsis-dot"}, "."),
            React.createElement("div", {className: "loading__ellipsis-dot"}, "."),
            React.createElement("div", {className: "loading__ellipsis-dot"}, ".")));
    }

    function LoadingStepBlock({
                                  state,
                                  title = "",
                                  distance = 0,
                                  fade = 0,
                                  start,
                                  finish,
                                  filesPrepared,
                                  filesPreparedMax = 0
                              }) {
        const stateMap = {
            waiting: "empty-circle",
            progress: "half-circle",
            done: "check-circle"
        };
        const colorMap = {
            waiting: "neutral",
            progress: "warning",
            done: "success"
        };
        const style = {
            opacity: 1 - fade * 0.225,
            transform: `translateY(${100 * distance}%)`
        };

        /**
         * Get a friendly-formatted date.
         * @param date Date
         */
        function dateFormatted(date) {
            return new Intl.DateTimeFormat("en-US", {
                dateStyle: "medium",
                timeStyle: "medium",
            }).format(date);
        }

        return (React.createElement("div", {
                className: `loading__step${state !== "waiting" ? " loading__step--in" : ""}`,
                style: style
            },
            React.createElement(Icon, {icon: stateMap[state], color: colorMap[state]}),
            React.createElement("div", null,
                React.createElement("div", {className: "loading__step-title"}, title),
                React.createElement("div", {className: "loading__step-info"},
                    start ? `${dateFormatted(start)} — ` : "",
                    finish ? dateFormatted(finish) : (state === "progress" ? React.createElement(LoadingEllipsis, null) : "")),
                React.createElement("div", {className: "loading__step-info"}, filesPrepared !== undefined ? `${filesPrepared} of ${filesPreparedMax} files prepared` : ""))));
    }
</script>

</body>

</html>